package com.gitee.loyo.act

import com.gitee.loyo.*
import java.math.BigDecimal
import java.time.LocalDate
import java.time.LocalDateTime
import java.util.concurrent.atomic.AtomicInteger
import kotlin.collections.ArrayList

open class Column<T: Any>(
    internal var columnName: String = "",
    internal var counter: AtomicInteger
){
    constructor(column: Column<*>) : this(column.getLabelName(), column.counter){
        model = column.model
        nameHandler = column.nameHandler

        primaryKey = column.primaryKey
        notNull = column.notNull
        length = column.length
        default = column.default
    }

    @Transient internal lateinit var model: Model<*, *>
    @Transient internal var nameHandler: (String) -> String = { it }
    @delegate: Transient internal val fieldName: String by lazy { KSQL.nameMappingStrategy.toFieldName(columnName) }
    internal var alias: String = ""

    @Transient internal var primaryKey: com.gitee.loyo.PrimaryKey? = null
    @Transient internal var notNull: com.gitee.loyo.NotNull? = null
    @Transient internal var length: com.gitee.loyo.Length? = null
    @Transient internal var default: com.gitee.loyo.Default? = null

    companion object{

        internal fun all(model: Model<*, *>): Column<*>{
            return Column<Any>("*", AtomicInteger()).apply{
                this.model = model
            }
        }
    }

    internal fun getLabelName(): String{
        val label = if(model.alias.isNotEmpty()){
            nameHandler("${model.alias}.$columnName")
        }else{
            nameHandler(columnName)
        }
        return if(alias.isEmpty()){
            label
        }else{
            "$label as $alias"
        }
    }

    open infix fun `as`(alias: String): Column<*>{
        return Column<T>(this).also {
            it.columnName = columnName
            it.alias = alias
        }
    }

    open infix fun `as`(column: Column<*>): Column<*>{
        return `as`(column.columnName)
    }

    open infix fun eq(value: T?): SQLPart{
        if(value == null){
            return SQLPart(counter.getAndIncrement(), "${getLabelName()} is null").also { model.addSQLPart(it) }
        }
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} = ?", value).also { model.addSQLPart(it) }
    }

    open infix fun notEq(value: T?): SQLPart {
        if(value == null){
            return SQLPart(counter.getAndIncrement(), "${getLabelName()} is not null").also { model.addSQLPart(it) }
        }
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} != ?", value).also { model.addSQLPart(it) }
    }

    open infix fun gt(value: T?): SQLPart {
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} > ?", value).also { model.addSQLPart(it) }
    }

    open infix fun gtEq(value: T?): SQLPart {
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} >= ?", value).also { model.addSQLPart(it) }
    }

    open infix fun lt(value: T?): SQLPart {
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} < ?", value).also { model.addSQLPart(it) }
    }

    open infix fun ltEq(value: T?): SQLPart {
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} <= ?", value).also { model.addSQLPart(it) }
    }

    open infix fun like(value: T?): SQLPart {
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} like ?", value).also { model.addSQLPart(it) }
    }

    open infix fun between(value: Pair<T?, T?>): SQLPart {
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} between ? and ?", value.first, value.second).also { model.addSQLPart(it) }
    }

    open infix fun `in`(array: Array<T>?): SQLPart {
        if(array == null)
            return SQLPart(counter.get(), "")
        return SQLPart(
            counter.getAndIncrement(),
            "${getLabelName()} in (${array.joinToString(", ") { "?" }})",
            *array
        ).also { model.addSQLPart(it) }
    }

}

class SQLPart(
    private var stack: Int,
    private val text: String,
    private vararg val param: Any?,
){
    private val buf: StringBuilder by lazy { StringBuilder(text) }
    private val params: ArrayList<Any?> by lazy { arrayListOf(*param) }
    private var enable = true
    private var connected = false

    fun getString(): String{
        if(!enable)
            throw UnsupportedOperationException("not enabled")
        return buf.toString()
    }

    fun getParameters(): ArrayList<Any?>{
        if(!enable)
            throw UnsupportedOperationException("not enabled")
        return params
    }

    fun isEnabled(): Boolean = enable
    fun isConnected(): Boolean = connected

    internal fun disable() {
        enable = false
    }

    infix fun and(other: SQLPart) : SQLPart {
        connect("and", other)
        return this
    }

    infix fun or(other: SQLPart) : SQLPart {
        connect("or", other)
        return this
    }

    private fun connect(connect: String, other: SQLPart){
        connected = true
        buf.append(" $connect ")
        if(stack + 1 < other.stack){
            buf.append("(")
        }
        if(other.buf.isEmpty()){
            buf.append(other.text)
        }else{
            buf.append(other.buf)
        }
        if(stack + 1 < other.stack){
            buf.append(")")
        }
        stack = other.stack

        if(params.isEmpty() && param != null){
            params.add(param)
        }
        if(other.params.isEmpty()){
            if(other.param != null) {
                params.add(other.param)
            }
        }else{
            params.addAll(other.params)
        }

        other.enable = false
    }

}

class LiteralColumn<T: Any>(column: Column<*>, val value: Any) : Column<T>(column.getLabelName(), column.counter) {

    init {
        this.model = column.model
        this.nameHandler = column.nameHandler
        this.alias = column.alias
        this.primaryKey = column.primaryKey
        this.notNull = column.notNull
        this.length = column.length
        this.default = column.default
    }

    private fun wrapString(literal: Any?): String{
        return if(literal is String){
            "'$literal'"
        }else{
            literal?.toString()?:"null"
        }
    }
    
    override infix fun eq(literal: T?): SQLPart{
        if(literal == null){
            return SQLPart(counter.getAndIncrement(), "${getLabelName()} is null").also { model.addSQLPart(it) }
        }
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} = ${wrapString(literal)}", value).also { model.addSQLPart(it) }
    }

    override infix fun notEq(literal: T?): SQLPart {
        if(literal == null){
            return SQLPart(counter.getAndIncrement(), "${getLabelName()} is not null").also { model.addSQLPart(it) }
        }
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} != ${wrapString(literal)}", value).also { model.addSQLPart(it) }
    }

    override infix fun gt(literal: T?): SQLPart {
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} > ${wrapString(literal)}", value).also { model.addSQLPart(it) }
    }

    override infix fun gtEq(literal: T?): SQLPart {
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} >= ${wrapString(literal)}", value).also { model.addSQLPart(it) }
    }

    override infix fun lt(literal: T?): SQLPart {
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} < ${wrapString(literal)}", value).also { model.addSQLPart(it) }
    }

    override infix fun ltEq(literal: T?): SQLPart {
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} <= ${wrapString(literal)}", value).also { model.addSQLPart(it) }
    }

    override infix fun like(literal: T?): SQLPart {
        return SQLPart(counter.getAndIncrement(), "${getLabelName()} like ${wrapString(literal)}", value).also { model.addSQLPart(it) }
    }

    override fun between(value: Pair<T?, T?>): SQLPart {
        throw UnsupportedOperationException()
    }

    override fun `in`(array: Array<T>?): SQLPart {
        throw UnsupportedOperationException()
    }

    override fun `as`(alias: String): LiteralColumn<*> {

        return LiteralColumn<T>(this, value).also {
            it.columnName = columnName
            it.alias = alias
        }
    }
}

class TINYINT(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<Boolean>(fieldName, counter)

class SMALLINT(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<Short>(fieldName, counter)

class INT(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<Int>(fieldName, counter)

class BIGINT(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<Long>(fieldName, counter)

class DECIMAL(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<BigDecimal>(fieldName, counter)

class FLOAT(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<Float>(fieldName, counter)

class DOUBLE(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<Double>(fieldName, counter)

class VARCHAR(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<String>(fieldName, counter)

class CHAR(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<String>(fieldName, counter)

class DATE(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<LocalDate>(fieldName, counter)

class DATETIME(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<LocalDateTime>(fieldName, counter)

class TIMESTAMP(
    fieldName: String = "",
    counter: AtomicInteger
) : Column<TIMESTAMP>(fieldName, counter)